Kubernetes 共享 GPU 调度指南
本篇博文介绍了如何在 Kubernetes 集群中使用 GPU 资源,以及如何设置共享 Nvidia GPU 的详细步骤。
依照本文操作你将完成如下内容:
- Nvidia 和 CUDA 驱动安装
- Nvidia Container Runtime 的配置
- Kubernetes 通过 GPU plugin 实现 GPU 调度支持
- Kubernetes 开启共享 Nvidia GPU 资源配置
前言
Kubernetes 已经成为云原生应用编排和管理的事实标准。在 v1.26 版本 Scheduling GPUs 进入 stable 状态,通过 device plugins 可以让 Pods 可以访问特定硬件如 GPUs。
越来越多的机器学习(ML)和人工智能(AI)领域的开发者希望利用 Kubernetes 平台的优势,来管理 GPU 调度任务。
相比本地 GPU 管理方案,Kubernetes 通过对 GPU 资源的高效分时复用和动态调度,从而降本增效。声明式的容器化配置,使部署过程可以固化和复用,避免了重复配置机器学习环境。同时容器隔离可以避单一进程占用过多共享资源,保障系统稳定性。
尽管本文以 Arch Linux 作为基本环境,但同样适用于其他发行版。
Nvidia 驱动安装
- NVIDIA Driver Installation Quickstart Guide
- 在开始安装 NVIDIA 驱动程序前请参照《 适用于 Linux 的 CUDA 安装指南》中的 2. Pre-installation Actions(预安装操作) 对环境进行预检查。
安装方式
NVIDIA 驱动程序提供三种格式:
推荐使用包管理器安装驱动,方便以后的维护和升级。
安装示例
以下是在 Arch Linux 上安装 Nvidia 驱动的示例:
Arch Linux Wiki for NVIDIAN 介绍了 Arch Linux 官方提供的 Nvidia 驱动包和内核的对应关系,如下所示:
- nvidia 软件包(用于 linux内核 )
- nvidia-lts 软件包(用于 linux-lts内核 )
- nvidia-dkms 软件包(用于所有其他内核)。
DKMS(Dynamic Kernel Module Support)是一个框架,用于在Linux系统中构建和安装内核模块。它允许在内核更新时自动重建已注册的内核模块。
下面以我的 Arch Linux 环境为例,我日常使用的内核为 linux ,同时也安装了 linux-lts 内核作为备用,我想要 Nvidia 驱动在这俩内核中都正常工作,所以需要安装 nvidia 和 nvidia-lts 命令如下:
# 查看显卡
lspci | grep -i nvidia | grep VGA
# 构建工具
sudo pacman -S --needed base-devel
# nvidia 驱动
sudo pacman -S nvidia nvidia-lts
其他发行版请参考安装方式中提到的 NVIDIA 驱动程序提供三种格式
进行安装。
驱动验证
完成驱动安装好执行 nvidia-smi 验证是否可用
❯ nvidia-smi
Wed Aug 16 17:04:35 2023
+---------------------------------------------------------------------------------------+
| NVIDIA-SMI 535.98 Driver Version: 535.98 CUDA Version: 12.2 |
|-----------------------------------------+----------------------+----------------------+
| GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|=========================================+======================+======================|
| 0 NVIDIA GeForce RTX 3080 ... Off | 00000000:01:00.0 Off | N/A |
| N/A 50C P8 11W / 115W | 8MiB / 16384MiB | 0% Default |
| | | N/A |
+-----------------------------------------+----------------------+----------------------+
+---------------------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=======================================================================================|
| 0 N/A N/A 1067 G /usr/bin/gnome-shell 3MiB |
+---------------------------------------------------------------------------------------+
❯ nvidia-smi --query-gpu=gpu_name --format=csv,noheader
NVIDIA GeForce RTX 3080 Ti Laptop GPU
❯ nvidia-smi -L
GPU 0: NVIDIA GeForce RTX 3080 Ti Laptop GPU (UUID: GPU-1b17e588-c712-9244-37de-93a8a77ebe6d)
上述驱动为单卡配置,其他情况请参考以下文档
- 多卡支持 NVIDIA Multi-Instance GPU User Guide
- 开启 VGPU 支持 VGPU
- 开启消费级显卡的 vgpu 功能 vgpu_unlock
- 更多文档 https://docs.nvidia.com/datacenter/tesla/index.html
CUDA 驱动安装
CUDA(Compute Unified Device Architecture)是 NVIDIA 推出的通用并行计算架构,该架构使 GPU 能够解决复杂的计算问题。
安装方式
CUDA 提供了两种安装方式,你可以使用两种不同的安装机制之一来安装 CUDA 工具包:
安装示例
以下是在 Arch Linux 上安装 CUDA 驱动的示例:
paru -S cuda
根据安装后操作配置环境变量。
Arch Linux 环境会将 CUDA 相关文件安装至 /opt/cuda
,将 CUDA 的 PATH 加到 rc
中(如~/.bashrc,~/.zshrc),此路径引永远指向最新版的 CUDA。
echo 'export CUDA_PATH=/opt/cuda
export PATH=$PATH:/opt/cuda/bin:/opt/cuda/nsight_compute:/opt/cuda/nsight_systems/bin'| sudo tee /etc/profile.d/cuda.sh
source /etc/profile
驱动验证
输入以下命令来检查 CUDA 版本:
❯ nvcc --version
nvcc: NVIDIA (R) Cuda compiler driver
Copyright (c) 2005-2023 NVIDIA Corporation
Built on Tue_Jun_13_19:16:58_PDT_2023
Cuda compilation tools, release 12.2, V12.2.91
Build cuda_12.2.r12.2/compiler.32965470_0
Container Runtime
这里选符合 oci 标准的运行时,本文选用 contianerd。
Containerd 安装
驱动加载
# 写入持久化配置
cat <<EOF | sudo tee /etc/modules-load.d/containerd.conf
overlay
br_netfilter
EOF
# 重载配置使其生效
sudo systemctl restart systemd-modules-load.service
# 或者手动加载
sudo modprobe overlay
sudo modprobe br_netfilte
安装软件包
paru -S containerd
sudo systemctl enable --now containerd
Containerd 验证
执行如下命令,进行验证
sudo ctr image pull docker.io/library/hello-world:latest
sudo ctr run --rm -t docker.io/library/hello-world:latest hello-world
Nvidia-Container-Runtime 安装
Containerd 默认使用 runc 不支持 GPU 设备,nvidia-container-runtime 是 runc 的修补版本,它添加了自定义 pre-start hook 从而在容器内启用 GPU 支持。
安装 NVIDIA Container Toolkit 会在 Containerd(或任何其他运行时)周围提供一个shim,用于处理 GPU 请求。需要 GPU 的容器创建由 nvidia-container-runtime 处理。
当检测到 NVIDIA_VISIBLE_DEVICES 环境变量时,会调用 libnvidia-container 库将 GPU Device 和 CUDA Driver 附加到容器,配置 GPU 访问权限后,NVIDIA 系统会调用常规容器运行时来继续其余的启动过程。如果没有检测到 NVIDIA_VISIBLE_DEVICES 则会执行默认的 runc。
以 NVIDIA 的 GPU 设备为例,运行 GPU 容器后容器内将会出现 GPU 设备和驱动目录:
- GPU 设备,比如 /dev/nvidia0;
- GPU 驱动目录,比如 /usr/local/nvidia/*。
NVIDIA Container Toolkit 是一个容器工具包,它包含了一系列的工具和组件,用于在GPU环境中管理和运行容器化应用程序:
- nvidia-container-runtime
NVIDIA公司开发的一种容器运行时,可以用于在GPU环境中运行容器化应用程序。 - NVIDIA 容器运行时挂钩 ( nvidia-container-toolkit/ nvidia-container-runtime-hook)
- NVIDIA 容器库和 CLI ( libnvidia-container1, nvidia-container-cli)
下面开始安装 NVIDIA 容器运行时
使用以下命令将完整的 NVIDIA Container Toolkit 添加到系统中:
NVIDIA 容器运行时依赖于容器映像中现有的 CUDA 库。其他图像仍然可以工作,但它们看不到 GPU 硬件。
paru -S nvidia-container-runtime nvidia-container-toolkit
配置 Containerd 使用 Nvidia-Container-Runtime
我们需要在启动 kubelet 之前将 Containerd 默认运行时从 runc 更改为 nvidia-container-runtime 。
生成默认配置文件
sudo mkdir /etc/containerd
sudo sh -c "containerd config default > /etc/containerd/config.toml"
添加 nvidia 容器运行时,并设为默认
version = 2
[plugins]
[plugins."io.containerd.grpc.v1.cri"]
[plugins."io.containerd.grpc.v1.cri".containerd]
default_runtime_name = "nvidia"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes]
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.nvidia]
privileged_without_host_devices = false
runtime_engine = ""
runtime_root = ""
runtime_type = "io.containerd.runc.v2"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.nvidia.options]
BinaryName = "/usr/bin/nvidia-container-runtime"
注意要和 kubelet 保持相同的 Cgroup 控制器,这里将 SystemdCgroup设为
true`。
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
SystemdCgroup = true
添加完成后重启 containerd 服务
sudo systemctl restart containerd
Nvidia-Container-Runtime 验证
sudo ctr images pull docker.io/nvidia/cuda:12.2.0-devel-ubuntu20.04
sudo ctr run --rm -t --gpus 0 docker.io/nvidia/cuda:12.2.0-devel-ubuntu20.04 nvidia-smi nvidia-smi
Thu Aug 24 06:43:06 2023
+---------------------------------------------------------------------------------------+
| NVIDIA-SMI 535.98 Driver Version: 535.98 CUDA Version: 12.2 |
|-----------------------------------------+----------------------+----------------------+
| GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|=========================================+======================+======================|
| 0 NVIDIA GeForce RTX 3080 ... Off | 00000000:01:00.0 Off | N/A |
| N/A 45C P0 29W / 115W | 10MiB / 16384MiB | 0% Default |
| | | N/A |
+-----------------------------------------+----------------------+----------------------+
+---------------------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=======================================================================================|
+---------------------------------------------------------------------------------------+
如果有多 runtime 共存需求,需要在 Kubernetes 中创建相应的 runtime class。
例如我们想让 containerd 默认 runtime 设置为 runc ,只在使用 GPU 设备时使用 Nvidia-Container-Runtime 该怎么做呢?
首先配置 containerd [plugins."io.containerd.grpc.v1.cri".containerd]
中 default_runtime_name
设置为 runc
。
然后 [plugins."io.containerd.grpc.v1.cri".containerd.runtimes]
下面多 runtime 配置创建 runtime class
资源:
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: nvidia
handler: nvidia
最后在 pod 资源清单中添加 runtimeClassName: nvidia
配置即可。
在 Kubernetes 中启用 GPU 支持
Kubernetes 通过设备插件(Device Plugins) 以允许 Pod 访问类似 GPU 这类特殊的硬件功能特性。不同厂商提供了不同的 Device Plugin,本小节部署 NVIDIA/k8s-device-plugin
来做基本演示。
kubectl create -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v0.14.0/nvidia-device-plugin.yml
# 查看日志
kubectl -n kube-system logs -f $(kubectl -n kube-system get pods -l name=nvidia-device-plugin-ds -o jsonpath="{.items[*].metadata.name}")
I0822 07:26:02.687308 1 main.go:256] Retreiving plugins.
I0822 07:26:02.687912 1 factory.go:107] Detected NVML platform: found NVML library
I0822 07:26:02.687946 1 factory.go:107] Detected non-Tegra platform: /sys/devices/soc0/family file not found
I0822 07:26:02.698806 1 server.go:165] Starting GRPC server for 'nvidia.com/gpu'
I0822 07:26:02.699008 1 server.go:117] Starting to serve 'nvidia.com/gpu' on /var/lib/kubelet/device-plugins/nvidia-gpu.sock
I0822 07:26:02.700674 1 server.go:125] Registered device plugin for 'nvidia.com/gpu' with Kubelet
运行 GPU 作业进行验收
部署 nvidia-device-plugin 后,容器现在可以使用 NVIDIA GPU nvidia.com/gpu 类型的 Extended Resource 。
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: gpu-pod
spec:
restartPolicy: Never
containers:
- name: cuda-container
image: nvcr.io/nvidia/k8s/cuda-sample:vectoradd-cuda10.2
resources:
limits:
nvidia.com/gpu: 1 # requesting 1 GPU
tolerations:
- key: nvidia.com/gpu
operator: Exists
effect: NoSchedule
EOF
查看日志结果
$ kubectl logs gpu-pod
[Vector addition of 50000 elements]
Copy input data from the host memory to the CUDA device
CUDA kernel launch with 196 blocks of 256 threads
Copy output data from the CUDA device to the host memory
Test PASSED
Done
可以通过 NVIDIA GPU Operator 可以避免在每个节点上手动安装和配置 nvidia drive 和 nvidia-container-runtime。从而简化 GPU 工作负载的管理和操作。
到此已依照英伟达官方 device plugin 实现了 Kubernetes 集群对 gpu 的支持。
现在每个容器可以请求一个或者多个 GPU,但多容器无法共享一块 GPU ,因为 limit 只能是整数来指定单卡数量,无法请求 GPU 的一小部分,结果是单个容器占用了整个 GPU,导致推理阶段 GPU 利用率极低。
下面我们来看看如何让更多容器可以共享 GPU 来提高利用率。
GPU 共享的可能性
硬件方案:vGPU 技术可以将显卡资源虚拟化成多块小显卡,会有部分性能损耗。通常需要特定的硬件支持,例如 NVIDIA GRID 卡或 AMD MxGPU 卡,并且需要购买额外的GPU许可证,会增加成本和管理负担。
软件方案:工作负载的调度是在软件层面完成,虽然可以利用整块 GPU 的算力,但无法对工作负载的显存进行限制,这意味着我们需要在代码中进行限制。例如 TensorFlow 允许按照此处的指令限制 GPU 功率。
在机器学习的背景下,训练期间可能会在一项训练作业中利用完整的 GPU 容量,但在推理期间很可能希望在可用的 GPU 上分布多个实例。
下面,我们利用软件层的 GPU 共享,将个工作负载扩展到单个 GPU 上。
你可以在下面文章中了解更多的共享方案
- Time-Slicing GPUs in Kubernetes
- 技术阐述 https://www.arrikto.com/blog/gpu-virtualization-in-k8s-challenges-and-state-of-the-art/
- 技术阐述 https://developer.nvidia.com/blog/improving-gpu-utilization-in-kubernetes/
- https://qiankunli.github.io/2021/08/18/gpu_utilization.html
- https://github.com/volcano-sh/devices
- https://github.com/volcano-sh/volcano/blob/master/docs/user-guide/how_to_use_gpu_sharing.md
- https://zhuanlan.zhihu.com/p/65543866
- https://houmin.cc/posts/cf391335/
- https://xie.infoq.cn/article/32b6e04e53419c35465acc699
安装 GPU sharing extension
我们将安装 阿里 GPU 共享调度扩展和阿里 GPU 共享设备插件。官方文档可见 安装指南。
1. 在控制平面部署GPU共享调度扩展器
kubectl create -f https://raw.githubusercontent.com/AliyunContainerService/gpushare-scheduler-extender/master/config/gpushare-schd-extender.yaml
2.修改 kube-scheduler 配置(Kubernetes v1.23+)
添加 lable
kubectl label node archlinux gpushare=true
kubectl label node archlinux node-role.kubernetes.io/master=
配置 kube-scheduler
cd /etc/kubernetes
sudo curl -O https://raw.githubusercontent.com/AliyunContainerService/gpushare-scheduler-extender/master/config/scheduler-policy-config.yaml
编辑 kube-scheduler.yaml
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
component: kube-scheduler
tier: control-plane
name: kube-scheduler
namespace: kube-system
spec:
containers:
- command:
- kube-scheduler
- --authentication-kubeconfig=/etc/kubernetes/scheduler.conf
- --authorization-kubeconfig=/etc/kubernetes/scheduler.conf
- --bind-address=127.0.0.1
- --kubeconfig=/etc/kubernetes/scheduler.conf
- --leader-elect=true
- --config=/etc/kubernetes/scheduler-policy-config.yaml
image: registry.k8s.io/kube-scheduler:v1.28.0
imagePullPolicy: IfNotPresent
livenessProbe:
failureThreshold: 8
httpGet:
host: 127.0.0.1
path: /healthz
port: 10259
scheme: HTTPS
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 15
name: kube-scheduler
resources:
requests:
cpu: 100m
startupProbe:
failureThreshold: 24
httpGet:
host: 127.0.0.1
path: /healthz
port: 10259
scheme: HTTPS
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 15
volumeMounts:
- mountPath: /etc/kubernetes/scheduler-policy-config.yaml
name: scheduler-policy-config
readOnly: true
- mountPath: /etc/kubernetes/scheduler.conf
name: kubeconfig
readOnly: true
hostNetwork: true
priority: 2000001000
priorityClassName: system-node-critical
securityContext:
seccompProfile:
type: RuntimeDefault
volumes:
- hostPath:
path: /etc/kubernetes/scheduler-policy-config.yaml
type: FileOrCreate
name: scheduler-policy-config
- hostPath:
path: /etc/kubernetes/scheduler.conf
type: FileOrCreate
name: kubeconfig
status: {}
注意在 v1.28 中 KubeSchedulerConfiguration api 版本放生了改变:
- KubeSchedulerConfiguration: v1beta2 ➜ v1.
以下为引用的部分 CHANGELOG-1.28
kube-scheduler component config (KubeSchedulerConfiguration) kubescheduler.config.k8s.io/v1beta2 is removed in v1.28. Migrate kube-scheduler configuration files to kubescheduler.config.k8s.io/v1. (#117649, @SataQiu)
默认情况下你会看到如下报错
❯ kubectl -n kube-system logs kube-scheduler-archlinux
I0911 07:15:36.217495 1 serving.go:348] Generated self-signed cert in-memory
E0911 07:15:36.217622 1 run.go:74] "command failed" err="no kind \"KubeSchedulerConfiguration\" is registered for version \"kubescheduler.config.k8s.io/v1beta2\" in scheme \"pkg/scheduler/apis/config/scheme/scheme.go:30\"
修正 api 版本后重启 kubelet 即可解决
sudo vim scheduler-policy-config.yaml
apiVersion: kubescheduler.config.k8s.io/v1
systemctl restart kubelet.service